﻿// (c) Copyright Damian Mehers http://damianblog.com/
// This source is subject to the Microsoft Public License.
// See http://www.microsoft.com/resources/sharedsource/licensingbasics/sharedsourcelicenses.mspx.
// All other rights reserved.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Web;
using MceFM.Last.MediaPlayer;

namespace MceFM.Last.FM {
  internal class Scrobbler {
    private const string clientId = "mfm";
    private const string clientVersion = "0.1";
    private const string loveHateUrl = "http://ws.audioscrobbler.com/1.0/rw/xmlrpc.php";

    private const string scrobbleInitializeUrl =
      "http://post.audioscrobbler.com/?hs=true&p=1.2&c={0}&v={1}&u={2}&t={3}&a={4}";

    private const string loveHateContentTemplate = @"<?xml version=""1.0"" encoding=""UTF-8""?>
<methodCall>
<methodName>{0}</methodName>
<params>
<param><value><string>{1}</string></value></param>
<param><value><string>{2}</string></value></param>
<param><value><string>{3}</string></value></param>
<param><value><string>{4}</string></value></param>
<param><value><string>{5}</string></value></param>
</params>
</methodCall>
";

    private string passwordHash;
    private string username;
    private string nowPlayingUrl;
    private string scrobbleUrl;
    private string sessionId;
    private readonly UserNotifier userNotifier;

    public Scrobbler(string username, string passwordHash, UserNotifier userNotifier) {
      this.username = username;
      this.passwordHash = passwordHash;
      this.userNotifier = userNotifier;
    }

    public string PasswordHash {
      get { return passwordHash; }
      set { passwordHash = value; sessionId = null; }
    }

    public string Username {
      get { return username; }
      set { username = value; sessionId = null; }
    }

    private static long ConvertToUnixTimestamp(DateTime date) {
      DateTime origin = new DateTime(1970, 1, 1, 0, 0, 0, 0);
      TimeSpan diff = date - origin;
      return (long)Math.Floor(diff.TotalSeconds);
    }

    private static string ToHexString(ICollection<byte> bytes) {
      StringBuilder sb = new StringBuilder(bytes.Count*2);
      foreach(byte b in bytes) {
        sb.AppendFormat("{0:x2}", b);
      }
      return sb.ToString();
    }

    private static string CreateChallenge(string passwordHash, long unixTimestamp) {
      MD5CryptoServiceProvider md5 = new MD5CryptoServiceProvider();
      string str = passwordHash + unixTimestamp;
      byte[] bytes = md5.ComputeHash(Encoding.UTF8.GetBytes(str));
      str = ToHexString(bytes);
      return str;
    }


    private void InitializeSession() {
      lock(this) {
        if (username == null || passwordHash == null) {
          sessionId = null;
          return;
        }

        if (sessionId != null) {
          return;
        }

        try {
          WebClient webClient = new WebClient();
          long timestamp = ConvertToUnixTimestamp(DateTime.Now);
          string auth = CreateChallenge(PasswordHash, timestamp);

          string url = string.Format(scrobbleInitializeUrl, clientId, clientVersion, Username, timestamp, auth);
          Trace.TraceInformation("Initializing Scrobble session: {0}", url);
          byte[] data = webClient.DownloadData(url);
          string response = Encoding.UTF8.GetString(data);

          StringReader stringReader = new StringReader(response);
          string ok = stringReader.ReadLine();
          if(ok != "OK") {
            Trace.TraceError(string.Format("Initializing Scrobble session, unexpected response:{0}", response));
            return;
          }
          sessionId = stringReader.ReadLine();
          nowPlayingUrl = stringReader.ReadLine();
          scrobbleUrl = stringReader.ReadLine();
        } catch(Exception ex) {
          Trace.TraceError("Error while initializing Scrobble session: {0}", ex);
        }
      }
    }


    private string currentTitle;
    private string currentAlbum;
    private string currentArtist;
    private DateTime currentSongStarted;
    private int currentDuration;
    private string currentLastfmAuth;

    public void TrackStoppedOrStartedHandler(Track track, TrackStoppedOrStartedEventArgs e) {
      if(e.Started) {
        HandleTrackStarted(track, e);
      } else {
        HandleTrackStopped(e);
      }
    }

    private void HandleTrackStarted(Track track,  TrackStoppedOrStartedEventArgs e) {
      InitializeCurrentInfo(track, e);

      for (int i = 0; i < 3; i++) {
        try {
          InitializeSession();
          if (sessionId == null) {
            return; // Failed to initialize
          }
          WebClient webClient = new WebClient();

          string nowPlayingData = string.Format("s={0}&a={1}&t={2}&b={3}&l={4}&m={5}",
                                                sessionId,
                                                HttpUtility.HtmlEncode(currentArtist),
                                                HttpUtility.HtmlEncode(currentTitle),
                                                HttpUtility.HtmlEncode(currentAlbum),
                                                "",
                                                "");

          webClient.Headers[HttpRequestHeader.UserAgent] = clientId;
          webClient.Headers[HttpRequestHeader.ContentType] = "application/x-www-form-urlencoded";
          Trace.TraceInformation("Sending NowPlaying message: {0}", nowPlayingData);
          string response = webClient.UploadString(nowPlayingUrl, "POST", nowPlayingData);
          Trace.TraceInformation("NowPlaying response is {0}", response);
          return;
        } catch (Exception ex) {
          Trace.TraceError("Error sending a NowPlaying message to Last.fm {0}", ex);
          sessionId = nowPlayingUrl = scrobbleUrl = null;
        }
      }
    }

    private void InitializeCurrentInfo(Track track, TrackStoppedOrStartedEventArgs e) {
      if (track == null) {
        currentTitle = e.Name;
        currentAlbum = e.Album;
        currentArtist = e.Artist;
        currentDuration = e.Duration;
      } else {
        currentTitle = track.Title;
        currentAlbum = track.Album;
        currentArtist = track.Creator;
        currentDuration = track.Duration / 1000;
        currentLastfmAuth = track.LastFmTrackAuth;
      }
      currentSongStarted = DateTime.Now;
    }

    private void HandleTrackStopped(TrackStoppedOrStartedEventArgs e) {

      // The track must have been played for a duration of at least 240 seconds or half 
      // the track's total length, whichever comes first. Skipping or pausing the track is 
      // irrelevant as long as the appropriate amount has been played. 
      Trace.TraceInformation("Deciding whether to scrobble {2}.  ElapsedSeconds={0} and duration is {1}", e.ElapsedSeconds, currentDuration, currentTitle);
      if (e.ElapsedSeconds > 240 || e.ElapsedSeconds > currentDuration / 2) {
        ScrobbleSong();
      }
      currentTitle = currentArtist = currentAlbum = currentLastfmAuth = null;
      currentSongStarted = DateTime.MinValue;
    }

    private void ScrobbleSong() {
      Trace.TraceInformation("Scrobbling {0}", currentTitle);

      string source = currentLastfmAuth == null ? "P" : "L" + currentLastfmAuth;

      for(int i = 0;i < 3;i++) {
        try {
          InitializeSession();
          if(sessionId == null) {
            return;
          }

          WebClient webClient = new WebClient();

          string nowPlayingData =
            string.Format("s={0}&a[0]={1}&t[0]={2}&i[0]={3}&o[0]={4}&r[0]=&l[0]={5}&b[0]={6}&n[0]=&m[0]=",
                          sessionId,
                          HttpUtility.HtmlEncode(currentArtist),
                          HttpUtility.HtmlEncode(currentTitle),
                          ConvertToUnixTimestamp(currentSongStarted),
                          source,
                          currentDuration,
                          HttpUtility.HtmlEncode(currentAlbum));

          webClient.Headers[HttpRequestHeader.UserAgent] = clientId;
          webClient.Headers[HttpRequestHeader.ContentType] = "application/x-www-form-urlencoded";
          Trace.TraceInformation("Sending Scrobble message: {0}", nowPlayingData);
          string response = webClient.UploadString(scrobbleUrl, "POST", nowPlayingData);
          Trace.TraceInformation("Scrobble response is {0}", response);
          return;
        } catch(Exception ex) {
          Trace.TraceError("Error sending a NowPlaying message to Last.fm {0}", ex);
          sessionId = nowPlayingUrl = scrobbleUrl = null;
        }
      }
    }


    private string LoveHate(string loveHate) {
      string tmpTitle = currentTitle;
      string tmpArtist = currentArtist;
      if (tmpTitle == null || tmpArtist == null) {
        Trace.TraceError("Love/Hate but no current song playing");
        return null;
      }
      long unixTimestamp = ConvertToUnixTimestamp(DateTime.Now);
      string method = loveHate;
      string challenge = unixTimestamp.ToString();
      string auth = CreateChallenge(PasswordHash, unixTimestamp);

      string content = string.Format(loveHateContentTemplate,
                                     method,
                                     Username,
                                     challenge,
                                     auth,
                                     HttpUtility.HtmlEncode(tmpArtist),
                                     HttpUtility.HtmlEncode(tmpTitle));
      using(WebClient webClient = new WebClient()) {
        webClient.Headers[HttpRequestHeader.ContentType] = "text/xml";
        webClient.Headers[HttpRequestHeader.UserAgent] = "MceFM 1.2 (Windows)";
        try {
          Trace.TraceInformation("Sending {0} to {1}", content, loveHateUrl);
          string response =
            webClient.UploadString(loveHateUrl, "POST", content);
          Trace.TraceInformation("LoveHate response is {0}", response);
        } catch(Exception ex) {
          Trace.TraceError(
            "Error sending {0}.  Exception is {1}.  Probably OK if it is complaining about the underlying connection closing.",
            content,
            ex);
        }
      }
      return string.Format("{0} by {1}", tmpTitle, tmpArtist);
    }

    public void Love() {
      string desc = LoveHate("loveTrack");
      userNotifier.Notify("Loved " + desc);
    }

    public void Hate() {
      string desc = LoveHate("banTrack");
      userNotifier.Notify("Banned " + desc);
    }


  }
}